/* filesystem.c
 * Filesystem utility routines
 *
 * (c) 2024, Lau
 *
 */

#include "pi_filesystem.h"

#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <dirent.h>
#include <sys/stat.h>

#ifdef _WIN32
#include <windows.h>
#include <tchar.h>
#include <shlobj.h>

#else /* _WIN32 */
#ifdef ENABLE_APPLICATION_BUNDLE
#include <mach-o/dyld.h>
#endif
#ifdef __linux__
#include <sys/utsname.h>
#endif
#ifdef __FreeBSD__
#include <sys/types.h>
#include <sys/sysctl.h>
#endif
#ifdef HAVE_DLGET
#include <dlfcn.h>
#endif
#include <pwd.h>
#endif /* _WIN32 */

#define PROFILES_DIR    "profiles"
#define PLUGINS_DIR_NAME    "plugins"

/*
 * Application configuration namespace. Used to construct configuration
 * paths and environment variables.
 * XXX We might want to use the term "application flavor" instead, with
 * "packet" and "log" flavors.
 */

char *persconffile_dir;
char *datafile_dir;
char *persdatafile_dir;
char *persconfprofile;
char *doc_dir;
char *current_working_dir;

/* Directory from which the executable came. */
static char *progfile_dir = "/etc/profile";

/*
 * Given a pathname, return a pointer to the last pathname separator
 * character in the pathname, or NULL if the pathname contains no
 * separators.
 */
char * find_last_pathname_separator(const char *path)
{
    char *separator;

#ifdef _WIN32
    char c;

    /*
     * We have to scan for '\' or '/'.
     * Get to the end of the string.
     */
    separator = strchr(path, '\0');     /* points to ending '\0' */
    while (separator > path) {
        c = *--separator;
        if (c == '\\' || c == '/')
            return separator;   /* found it */
    }

    /*
     * OK, we didn't find any, so no directories - but there might
     * be a drive letter....
     */
    return strchr(path, ':');
#else
    separator = strrchr(path, '/');
    return separator;
#endif
}

/*
 * Given a pathname, return the last component.
 */
const char * get_basename(const char *path)
{
    const char *filename;

    if (path == NULL) {
        return NULL;
    }
    filename = find_last_pathname_separator(path);
    if (filename == NULL) {
        /*
         * There're no directories, drive letters, etc. in the
         * name; the pathname *is* the file name.
         */
        filename = path;
    } else {
        /*
         * Skip past the pathname or drive letter separator.
         */
        filename++;
    }
    return filename;
}

/*
 * Given a pathname, return a string containing everything but the
 * last component.  NOTE: this overwrites the pathname handed into
 * it....
 */
char * get_dirname(char *path)
{
    char *separator;

    if (path == NULL) {
        return NULL;
    }
    separator = find_last_pathname_separator(path);
    if (separator == NULL) {
        /*
         * There're no directories, drive letters, etc. in the
         * name; there is no directory path to return.
         */
        return NULL;
    }

    /*
     * Get rid of the last pathname separator and the final file
     * name following it.
     */
    *separator = '\0';

    /*
     * "path" now contains the pathname of the directory containing
     * the file/directory to which it referred.
     */
    return path;
}

/*
 * Given a pathname, return:
 *
 *  the errno, if an attempt to "stat()" the file fails;
 *
 *  EISDIR, if the attempt succeeded and the file turned out
 *  to be a directory;
 *
 *  0, if the attempt succeeded and the file turned out not
 *  to be a directory.
 */

int is_directory(const char *path)
{
    struct stat64 fstat;

    if (stat64(path, &fstat) < 0)
        return errno;

    if (S_ISDIR(fstat.st_mode))
        return EISDIR;
    else
        return 0;
}

int is_fifo(const char *path)
{
    struct stat64 fstat;

    if (stat64(path, &fstat) < 0)
        return errno;

    if (S_ISFIFO(fstat.st_mode))
        return ESPIPE;
    else
        return 0;
}

bool is_regular_file(const char *path)
{
    struct stat64 fstat;

    if (!path) {
        return false;
    }

    if (stat64(path, &fstat) != 0)
        return false;

    return S_ISREG(fstat.st_mode);
}

/*
 * Construct the path name of a non-extcap Wireshark executable file,
 * given the program name.  The executable name doesn't include ".exe";
 * append it on Windows, so that callers don't have to worry about that.
 *
 * This presumes that all non-extcap executables are in the same directory.
 *
 * The returned file name was g_malloc()'d so it must be g_free()d when the
 * caller is done with it.
 */
char * get_executable_path(const char *program_name)
{
    /*
     * Fail if we don't know what directory contains the executables.
     */
    if (progfile_dir == NULL)
        return NULL;

    char *path = malloc(256);
    snprintf(path, 256, "%s/%s.exe", progfile_dir, program_name);
    return path;
}

/*
 * Get the directory in which the program resides.
 */
const char * get_progfile_dir(void)
{
    return progfile_dir;
}

/*
 * Find the directory where the plugins are stored.
 *
 * On Windows, we use the plugin\{VERSION} subdirectory of the datafile
 * directory, where {VERSION} is the version number of this version of
 * Wireshark.
 *
 * On UN*X:
 *
 *    if we appear to be run from the build directory, we use the
 *    "plugin" subdirectory of the datafile directory;
 *
 *    otherwise, if the WIRESHARK_PLUGIN_DIR environment variable is
 *    set and we aren't running with special privileges, we use the
 *    value of that environment variable;
 *
 *    otherwise, if we're running from an app bundle in macOS, we
 *    use the Contents/PlugIns/wireshark subdirectory of the app bundle;
 *
 *    otherwise, we use the PLUGIN_DIR value supplied by the
 *    configure script.
 */
static char *plugin_dir = "./plugins/LuaScripts";
static char *plugin_dir_with_version;

//static void init_plugin_dir(void)
//{
//#if defined(HAVE_PLUGINS) || defined(HAVE_LUA)

//#endif /* defined(HAVE_PLUGINS) || defined(HAVE_LUA) */
//}

//static void init_plugin_pers_dir(void)
//{
//#if defined(HAVE_PLUGINS) || defined(HAVE_LUA)
//#ifdef _WIN32
//    plugin_pers_dir = get_persconffile_path(PLUGINS_DIR_NAME, false);
//#else
//    plugin_pers_dir = g_build_filename(g_get_home_dir(), ".local/lib",
//                                       CONFIGURATION_NAMESPACE_LOWER, PLUGINS_DIR_NAME, (char *)NULL);
//#endif
//#endif /* defined(HAVE_PLUGINS) || defined(HAVE_LUA) */
//}

/*
 * Get the directory in which the plugins are stored.
 */
const char * get_plugins_dir(void)
{
//    if (!plugin_dir)
//        init_plugin_dir();
    return plugin_dir;
}

//const char * get_plugins_dir_with_version(void)
//{
//    if (!plugin_dir)
//        init_plugin_dir();
//    if (plugin_dir && !plugin_dir_with_version)
//        plugin_dir_with_version = g_build_filename(plugin_dir, PLUGIN_PATH_ID, (char *)NULL);
//    return plugin_dir_with_version;
//}

/*
 * Get the directory in which files that, at least on UNIX, are
 * system files (such as "/etc/ethers") are stored; on Windows,
 * there's no "/etc" directory, so we get them from the global
 * configuration and data file directory.
 */
const char * get_systemfile_dir(void)
{
#ifdef _WIN32
    return "./";
#else
    return "/etc";
#endif
}

/*
 * Return an error message for UNIX-style errno indications on open or
 * create operations.
 */
const char * file_open_error_message(int err, bool for_writing)
{
    const char *errmsg;
    static char errmsg_errno[1024+1];

    switch (err) {
    case ENOENT:
        if (for_writing)
            errmsg = "The path to the file \"%s\" doesn't exist.";
        else
            errmsg = "The file \"%s\" doesn't exist.";
        break;
    case EACCES:
        if (for_writing)
            errmsg = "You don't have permission to create or write to the file \"%s\".";
        else
            errmsg = "You don't have permission to read the file \"%s\".";
        break;
    case EISDIR:
        errmsg = "\"%s\" is a directory (folder), not a file.";
        break;
    case ENOSPC:
        errmsg = "The file \"%s\" could not be created because there is no space left on the file system.";
        break;
#ifdef EDQUOT
    case EDQUOT:
        errmsg = "The file \"%s\" could not be created because you are too close to, or over, your disk quota.";
        break;
#endif
    case EINVAL:
        errmsg = "The file \"%s\" could not be created because an invalid filename was specified.";
        break;
#ifdef ENAMETOOLONG
    case ENAMETOOLONG:
        /* XXX Make sure we truncate on a character boundary. */
        errmsg = "The file name \"%.80s"  "\" is too long.";
        break;
#endif
    case ENOMEM:
        /*
         * The problem probably has nothing to do with how much RAM the
         * user has on their machine, so don't confuse them by saying
         * "memory".  The problem is probably either virtual address
         * space or swap space.
         */
#if GLIB_SIZEOF_VOID_P == 4
        /*
         * ILP32; we probably ran out of virtual address space.
         */
#define ENOMEM_REASON "it can't be handled by a 32-bit application"
#else
        /*
         * LP64 or LLP64; we probably ran out of swap space.
         */
#if defined(_WIN32)
        /*
         * You need to make the pagefile bigger.
         */
#define ENOMEM_REASON "the pagefile is too small"
#elif defined(ENABLE_APPLICATION_BUNDLE)
        /*
         * dynamic_pager couldn't, or wouldn't, create more swap files.
         */
#define ENOMEM_REASON "your system ran out of swap file space"
#else
        /*
         * Either you have a fixed swap partition or a fixed swap file,
         * and it needs to be made bigger.
         *
         * This is UN*X, but it's not macOS, so we assume the user is
         * *somewhat* nerdy.
         */
#define ENOMEM_REASON "your system is out of swap space"
#endif
#endif /* GLIB_SIZEOF_VOID_P == 4 */
        if (for_writing)
            errmsg = "The file \"%s\" could not be created because " ENOMEM_REASON ".";
        else
            errmsg = "The file \"%s\" could not be opened because " ENOMEM_REASON ".";
        break;
    default:
        snprintf(errmsg_errno, sizeof(errmsg_errno),
               "The file \"%%s\" could not be %s: %s.",
               for_writing ? "created" : "opened",
               strerror(err));
        errmsg = errmsg_errno;
        break;
    }
    return errmsg;
}

/*
 * Return an error message for UNIX-style errno indications on write
 * operations.
 */
const char * file_write_error_message(int err)
{
    const char *errmsg;
    static char errmsg_errno[1024+1];

    switch (err) {
    case ENOSPC:
        errmsg = "The file \"%s\" could not be saved because there is no space left on the file system.";
        break;
#ifdef EDQUOT
    case EDQUOT:
        errmsg = "The file \"%s\" could not be saved because you are too close to, or over, your disk quota.";
        break;
#endif
    default:
        snprintf(errmsg_errno, sizeof(errmsg_errno),
               "An error occurred while writing to the file \"%%s\": %s.",
               strerror(err));
        errmsg = errmsg_errno;
        break;
    }
    return errmsg;
}


bool file_exists(const char *fname)
{
    struct stat64 fstat;

    if (!fname) {
        return false;
    }

    if (stat64(fname, &fstat) != 0 && errno == ENOENT) {
        return false;
    } else {
        return true;
    }
}

bool config_file_exists_with_entries(const char *fname, char comment_char)
{
    bool start_of_line = true;
    bool has_entries = false;
    FILE *file;
    int c;

    if (!fname) {
        return false;
    }

    if ((file = fopen(fname, "r")) == NULL) {
        return false;
    }

    do {
        c = getc(file);
        if (start_of_line && c != comment_char && !isspace(c) && isprint(c)) {
            has_entries = true;
            break;
        }
        if (c == '\n' || !isspace(c)) {
            start_of_line = (c == '\n');
        }
    } while (c != EOF);

    fclose(file);
    return has_entries;
}
